Data Binding Collection Data with ZUML Annotations
This article is out of date, please refer to ZK Developer's Reference: Data Binding for more up to date information.
Henri Chen, Principal Engineer, Potix Corporation
March 29, 2007
Applicable to ZK 2.3 and later.
The Purpose
In the previous articles( this and this), we have discussed how to write zero code two way data binding with simple one-to-one case. In this article, we will discuss more complex cases, binding collection data to "Collection Component" such as Listbox and Grid component. Notice that, when I say "complex", it is in the view of "I", the one that implements the collection data binding. When it comes to the application developer's point, it is just as easy as any ZK data binding :-).
The day before
It has been many different ways to display collection data. The earliest and direct way was writing java codes to iterate the collection and new Listitems and Listcells accordingly and then attached to the parent Listbox. Following is the example.
<window>
<listbox id="lbx" rows="4"/>
<zscript>
for(Iterator it = persons.iterator(); it.hasNext();) {
Person person = (Person) it.next();
Listitem li = new Listitem();
new Listcell(person.getFirstName()).setParent(li);
new Listcell(person.getLastName()).setParent(li);
li.setParent(lbx);
}
</zscript>
</window>
Then we have "forEach" and "each" ZK attributes. The ZK loader understands the two special attributes so it iterates the collection data for you and creates Listitems and Listcells accordingly.
<window>
<listbox rows="4">
<listitem forEach="${persons}">
<listcell label="${each.firstName}"/>
<listcell label="${each.lastName}"/>
</listitem>
</listbox>
</window>
After that we have this "Live" data model mechanism. You associate the collection data to the Listbox's ListModel, and the listbox will pull the data in from the ListModel and have the ListitemRenderer to "draw" the contents automatically.
<window>
<zscript>
public class MyItemRenderer implements ListitemRenderer {
public void render(Listitem li, Object data) {
new Listcell(((Person)data).getFirstName()).setParent(li);
new Listcell(((Person)data).getLastName()).setParent(li);
}
}
ListModel model = new SimpleListModel(persons.toArray());
ListItemRenderer itemRenderer = new MyItemRenderer();
</zscript>
<listbox model="${model}" itemRenderer="${itemRenderer}" rows="4"/>
</window>
The "live collection" ListModel
As you can see, the previous methods can only be used to automate the "showing" of the collection data. If you wanted to modify the collection you still have to write Java codes to modify the Listbox and to write Java codes to change the collection data simultaneously. Then we implemented the "Live collection" ListModels (See ListModelList, ListModelSet, ListModelMap, ListModelArray). When you add, remove, and/or modify the "Live Collection" ListModel, not only the contents of the Listbox will change accordingly, the inner collection data of the ListModel can be changed, too. So now you just manipulate the "live collection" ListModel, and both Listbox(the view) and the associated inner collection(the data. i.e. persons list in the example) will change simultaneously and automatically.
<window>
<zscript>
public class MyItemRenderer implements ListitemRenderer {
public void render(Listitem li, Object data) {
new Listcell(person.getFirstName()).setParent(li);
new Listcell(person.getLastName()).setParent(li);
}
}
ListModel model = ListModelList.instance(persons);
ListItemRenderer itemRenderer = new MyItemRenderer();
</zscript>
<listbox id="lbx" model="${model}" itemRenderer="${itemRenderer}" rows="4"/>
<!-- when click, the persons list removes an entry and the Listbox refreshed accordingly -->
<button label="Remove Selected"
onClick="((List)lbx.getModel()).remove(lbx.getSelectedIndex())"/>
</window>
However, the mechanism of "live collection" ListModel is absent of a convenient feature that of "forEach" has. In "forEach" mechanism, we can "teach" ZKloader how to construct a Listitem by an "example" template. Multi-columns Listbox is naturally constructed based on that Listitem template. On the other hand, the "live data" ListModel mechanism would need us to write a ListItemRenderer to handle different customized situation.
Data binding collection data to Listbox
When applying data binding on the collection data, we have to choose the ListModel mechanism. Only that the data binder has a target attribute "model" to associate with in ZUML annotations. And only that ListModel mechanism it is possible to support two way data binding. The "model" attribute of the Listbox is supposed to accept a ListModel object. The ZK data binder would allow associate a Collection, say List, Set, Map, and Object array to the "model" attribute. ZK support a ListModelConverter to do such conversion automatically that would convert List, Set, Map, and Object Array to ListModelList, ListModelSet, ListModelMap, and ListModelArray, respectively.
Then here comes a problem. How do we handle the "ListItemRenderer" issue? If we require the ZK application developer to write a ListitemRenderer for each different data binding cases, the whole "zero code" data binding idea is collapsed. Fortunately, we found a way to avoid such pitfalls. We implemented a generic "BindingListitemRenderer" that will accept a Listitem "example" template and would construct the "real" Listitem one by one with pulled in data based on that template. Following is the example.
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<window xmlns:a="http://www.zkoss.org/2005/zk/annotation">
<a:bind model="persons" selectedItem="selected"/>
<listbox rows="4">
<a:bind _var="person"/>
<listitem>
<a:bind label="person.firstName"/>
<listcell/>
<a:bind label="person.lastName"/>
<listcell/>
</listitem>
</listbox>
</window>
In the example, the Listbox "model" attribute is data bound to a Java List "persons". When loading, the data binder would convert the Java collection list into ListModelList, a "live collection" ListModel, and "loads" it into "model" attribute of the Listbox. The special "_var" attribute that bound to the Listitem is the entry variable used in constructing the real Listitem. That is, when the BindingListitemRenderer construct the real Listitem, the pulled in data would be associated with this variable name and used in the Listitem and its children Listcells. In the example, the _var "person" variable would be used for each entry in the collection variable "persons" and used in constructing the two children Listcells, "person.firstName" and "person.lastName". The special "_var" attribute is very important here since Data Binder use it to distinguish a "template" Listitem from "real" Listitem.
The selectedItem of the Listbox
As you can see, the "selectedItem" attribute of the Listbox is data bound to a variable "selected". The Listbox "selecteItem" attribute is supposed to accept a Listitem. However, again, ZK apply a SelectedItemConverter that would convert the "selected Listitem" to the associated entry in the ListModel and vice versa. In the example, it would be the selected "person". Then, you can futher databound the "selected" variable to show the details of the selected "person" and a List-Detail type of application is easily implemented. The following is a test example for such requirements.
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<window xmlns:a="http://www.zkoss.org/2005/zk/annotation">
<a:bind model="persons" selectedItem="selected"/>
<listbox rows="4">
<a:bind _var="person"/>
<listitem>
<a:bind label="person.firstName"/>
<listcell/>
<a:bind label="person.lastName"/>
<listcell/>
</listitem>
</listbox>
<!-- show the detail of the selected person -->
<grid>
<rows>
<row>First Name: <a:bind value="selected.firstName"/><label/></row>
<row>Last Name: <a:bind value="selected.lastName"/><label/></row>
<row>Email: <a:bind value="selected.email"/><label/></row>
</rows>
</grid>
</window>
Load-On-Save
Not only that you can "show" the details of the selected object, users can also edit the selected bean and the ZK data binder would automatically reflect the change back into the list as the following example. We call this "load-on-save"; i.e. the associated Listbox content is reloaded when you do save on the selected bean.
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<window xmlns:a="http://www.zkoss.org/2005/zk/annotation">
<a:bind model="persons" selectedItem="selected"/>
<listbox rows="4">
<a:bind _var="person"/>
<listitem>
<a:bind label="person.firstName"/>
<listcell/>
<a:bind label="person.lastName"/>
<listcell/>
</listitem>
</listbox>
<!-- show the detail of the selected person -->
<grid>
<rows>
<row>First Name: <a:bind value="selected.firstName"/><textbox/></row>
<row>Last Name: <a:bind value="selected.lastName"/><textbox/></row>
<row>Email: <a:bind value="selected.email"/><label/></row>
</rows>
</grid>
</window>
The ZK data binder would see if some changed properties of a data bean would affect the other. For example, if users change the firstName or lastName of a person, the person's fullName should change accordingly. The computer would not be able to know such "semantic" level knowledge. Basically, the current algorithm would traverse the data bound properties of the Person bean and do load on each one. This kind of algorithm now seems work OK. However, for a complex data bean with many properties, it could be "waste of time" in travering properties. Maybe we should make it configurable in the future.
Collection in Collection
Collection in Collection or not in Collection is the same. The ZK data binder takes care this special condition and make it work seamlessly. Following is a test example for Listbox in Listbox. You can also try to change the email of the selected item and see how it reflect to the details.
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<window xmlns:a="http://www.zkoss.org/2005/zk/annotation">
<a:bind model="persons" selectedItem="selected"/>
<listbox rows="4">
<a:bind _var="person"/>
<listitem>
<a:bind label="person.firstName"/>
<listcell/>
<a:bind label="person.lastName"/>
<listcell/>
<listcell>
<a:bind model="emails" selectedItem="person.email"/>
<listbox mold="select" rows="1"/>
</listcell>
</listitem>
</listbox>
<!-- show the detail of the selected person -->
<grid>
<rows>
<row>First Name: <a:bind value="selected.firstName"/><textbox/></row>
<row>Last Name: <a:bind value="selected.lastName"/><textbox/></row>
<row>Email: <a:bind value="selected.email"/><label/></row>
</rows>
</grid>
</window>
Summary
The collection data binding has proved to work properly. We have implemented such meachanism on Listbox and Grid components. The next step would be applying the same mechanism to other "collection" type of ZK components such as Combobox. Another important thing would be to handle the "sorting" of the Listbox and Grid. The current Listbox sorting implementation applys directly on the Listitems and the ZK binder has not handle this issue yet. So you cannot apply "sorting" on the data binding listbox without proper coding. We will investigate on this area and make it works seamlessly in the near future.
Download the example code here.
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |